package edu.hawaii.myisern.brown.model;

import static edu.hawaii.myisern.brown.model.MyIsernModel.makeId;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.math.BigInteger;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.xml.bind.JAXBException;
import org.junit.Before;
import org.junit.Test;
import edu.hawaii.myisern.brown.model.MyIsernModel.CollabLevelCompare;
import edu.hawaii.myisern.collaborations.jaxb.Collaboration;
import edu.hawaii.myisern.organizations.jaxb.Organization;
import edu.hawaii.myisern.researchers.jaxb.Researcher;

/**
 * A test class showing the possible design of the class we'd like to
 * use to store our data.
 * 
 * @author Brian Jaress
 */
public class TestMyIsernModel {

  private static final String FAKE_NAME = "Fake Name";
  private static final String EXPECTED_EXCEPTION = "Didn't throw expected exception";
  private static final String NON_ID = "This is not an ID";

  // IDs from the sample data
  private static final String COLLAB_ID = "UM-UH-HPCS";
  private static final String ORG_ID = "University_of_Hawaii";
  private static final String RESEARCHER_ID = "Philip_Johnson";

  /**
   * A test model to be filled with correct data for testing queries.
   */
  public MyIsernModel model;

  private final String xmlDir;

  /**
   * Creates a test suite for MyIsernModel.
   */
  public TestMyIsernModel() {
    this.xmlDir = System.getProperty("user.dir") + "/xml/examples/";
  }

  /**
   * Prepare a model from the sample data.
   * 
   * @throws IOException on bad data
   * @throws JAXBException if a parser cannot be created
   */
  @Before
  public void prepareModel() throws JAXBException, IOException {
    this.model = MyIsernModel.getInstance();
    this.model.setCollaborationsFile(xmlDir
        + "collaborations.example.xml");
    this.model.setResearchersFile(xmlDir + "researchers.example.xml");
    this.model.setOrganizationsFile(xmlDir
        + "organizations.example.xml");
    this.model.load();
  }

  /**
   * Checks the overall consistency and spot-checks the completeness of
   * the loaded data.
   */
  @Test
  public void testDataLoaded() {
    Set<String> ids = model.getIds();
    assertTrue("A researcher ID", ids.contains(RESEARCHER_ID));
    assertTrue("An organization ID", ids.contains(ORG_ID));
    assertTrue("A collaboration ID", ids.contains(COLLAB_ID));
  }

  /**
   * Test queries that return researchers.
   */
  @Test
  public void testResearcherQuery() {
    // map of IDs to names
    Map<String, String> researchers = this.model.getResearchers();
    assertTrue("Researcher ID", researchers.containsKey(RESEARCHER_ID));
    Researcher r = new Researcher();
    this.model.fillResearcher(r, RESEARCHER_ID);
    assertEquals("Researcher name", r.getName(), "Philip Johnson");
  }

  /**
   * Test queries that return organizations.
   */
  @Test
  public void testOrganizationQueries() {
    // map of IDs to names
    Map<String, String> orgs = this.model.getOrganizations();
    assertTrue("Org ID", orgs.containsKey(ORG_ID));
    Organization o = new Organization();
    this.model.fillOrganization(o, ORG_ID);
    assertEquals("Org name", o.getName(), "University of Hawaii");

    // queries
    orgs = this.model.getOrganizationsByCollaborationLevel(
        CollabLevelCompare.EQUAL, 1);
    assertEquals("A single collaboration between two orgs", 2, orgs
        .size());
    assertTrue("Organization involved in collaboration", orgs
        .containsKey(ORG_ID));
    orgs = this.model.getOrganizationsByCollaborationLevel(
        CollabLevelCompare.GREATER, 1);
    assertTrue("No orgs with more than one collaboration", orgs
        .isEmpty());
  }

  /**
   * Test queries that return collaborations.
   */
  @Test
  public void testCollaborations() {
    // map of all collaborations IDs and names
    Map<String, String> collabs = this.model.getCollaborations();
    assertTrue("Collab ID", collabs.containsKey(COLLAB_ID));
    Collaboration c = new Collaboration();
    this.model.fillCollaboration(c, COLLAB_ID);
    assertEquals("Collab name", c.getName(), COLLAB_ID);

    // queries
    collabs = this.model.getCollaborationsByOrganization(ORG_ID);
    assertEquals("One collaboration for the organization", 1, collabs
        .size());
    collabs = this.model.getCollaborationsByOrganization(NON_ID);
    assertTrue("No collaborations", collabs.isEmpty());

    collabs = this.model.getCollaborationsByYear(2005);
    assertEquals("One collaboration in 2005", 1, collabs.size());
    collabs = this.model.getCollaborationsByYear(1776);
    assertTrue("No collaborations in 1776", collabs.isEmpty());

    collabs = this.model.getCollaborationsByResearcher(RESEARCHER_ID);
    assertEquals("One collaboration in 2005", 1, collabs.size());
    collabs = this.model.getCollaborationsByResearcher(NON_ID);
    assertTrue("No collaborations", collabs.isEmpty());
  }

  /**
   * Test loading from a single file which is expected to fail.
   * 
   * @param filePath the file path, which should contain the type of
   * data in the file (e.g. "researchers.xml")
   * @throws JAXBException on parser error
   * @throws IOException if input or output fails
   */
  private void badFileLoad(String filePath) throws JAXBException,
      IOException {

    this.model.setResearchersFile(xmlDir + "researchers.example.xml");
    this.model.setOrganizationsFile(xmlDir
        + "organizations.example.xml");
    this.model.setCollaborationsFile(xmlDir
        + "collaborations.example.xml");
    if (filePath.contains("researchers")) {
      this.model.setResearchersFile(filePath);
    }
    else if (filePath.contains("organizations")) {
      this.model.setOrganizationsFile(filePath);
    }
    else if (filePath.contains("collaborations")) {
      this.model.setCollaborationsFile(filePath);
    }
    this.model.load();
  }

  /**
   * Check that an exception is thrown on loading blank researcher names
   * 
   * @throws IOException on bad input
   * @throws JAXBException if a parser cannot be created
   */
  @Test(expected = IOException.class)
  public void testBlankNameResearchers() throws JAXBException,
      IOException {
    badFileLoad(xmlDir + "researchers.blank-names.bad.xml");
    fail(EXPECTED_EXCEPTION);
  }

  /**
   * Check that an exception is thrown on loading blank organization
   * names
   * 
   * @throws IOException on bad input
   * @throws JAXBException if a parser cannot be created
   */
  @Test(expected = IOException.class)
  public void testBlankNameOrganizations() throws JAXBException,
      IOException {
    badFileLoad(xmlDir + "organizations.blank-names.bad.xml");
    fail(EXPECTED_EXCEPTION);
  }

  /**
   * Check that an exception is thrown on loading blank collaboration
   * names
   * 
   * @throws IOException on bad input
   * @throws JAXBException if a parser cannot be created
   */
  @Test(expected = IOException.class)
  public void testBlankNameCollaborations() throws JAXBException,
      IOException {
    badFileLoad(xmlDir + "collaborations.blank-names.bad.xml");
    fail(EXPECTED_EXCEPTION);
  }

  /**
   * Check that an exception is thrown on loading duplicate researcher
   * names
   * 
   * @throws IOException on bad input
   * @throws JAXBException if a parser cannot be created
   */
  @Test(expected = IOException.class)
  public void testDuplicateResearcherName() throws JAXBException,
      IOException {
    badFileLoad(xmlDir + "researchers.duplicate-names.bad.xml");
    fail(EXPECTED_EXCEPTION);
  }

  /**
   * Check that an exception is thrown on loading duplicate organization
   * names
   * 
   * @throws IOException on bad input
   * @throws JAXBException if a parser cannot be created
   */
  @Test(expected = IOException.class)
  public void testDuplicateOrganizationName() throws JAXBException,
      IOException {
    badFileLoad(xmlDir + "organizations.duplicate-names.bad.xml");
    fail(EXPECTED_EXCEPTION);
  }

  /**
   * Check that an exception is thrown on loading duplicate
   * collaboration names
   * 
   * @throws IOException on bad input
   * @throws JAXBException if a parser cannot be created
   */
  @Test(expected = IOException.class)
  public void testDuplicateCollaborationName() throws JAXBException,
      IOException {
    badFileLoad(xmlDir + "collaborations.duplicate-names.bad.xml");
    fail(EXPECTED_EXCEPTION);
  }

  /**
   * Check that an exception is thrown on loading years that are too far
   * in the future.
   * 
   * @throws IOException on bad input
   * @throws JAXBException if a parser cannot be created
   */
  @Test(expected = IOException.class)
  public void testYearTooBig() throws JAXBException, IOException {
    badFileLoad(xmlDir + "collaborations.big-year.bad.xml");
    fail(EXPECTED_EXCEPTION);
  }

  /**
   * Check that an exception is thrown on loading years that are too far
   * in the past.
   * 
   * @throws IOException on bad input
   * @throws JAXBException if a parser cannot be created
   */
  @Test(expected = IOException.class)
  public void testYearTooSmall() throws JAXBException, IOException {
    badFileLoad(xmlDir + "collaborations.small-year.bad.xml");
    fail(EXPECTED_EXCEPTION);
  }

  /**
   * Test adding a researcher.
   * 
   * @throws IOException on duplicate ID
   */
  @Test
  public void testAddResearcher() throws IOException {
    Researcher r = new Researcher();
    r.setName(FAKE_NAME);
    this.model.storeResearcher(r);
    assertTrue("Inserted Researcher", this.model.getResearchers()
        .containsValue(FAKE_NAME));
  }

  /**
   * Test adding an organization
   * 
   * @throws IOException on duplicate ID
   */
  @Test
  public void testAddOrganization() throws IOException {
    Organization o = new Organization();
    o.setName(FAKE_NAME);
    this.model.storeOrganization(o);
    assertTrue("Inserted Organization", this.model.getOrganizations()
        .containsValue(FAKE_NAME));
  }

  /**
   * Test adding a collaboration
   * 
   * @throws IOException on duplicate ID
   */
  @Test
  public void testAddCollaboration() throws IOException {
    Collaboration c = new Collaboration();
    c.setName(FAKE_NAME);
    c.getYears().add(BigInteger.valueOf(2007));
    this.model.storeCollaboration(c);
    assertTrue("Inserted Collaboration", this.model.getCollaborations()
        .containsValue(FAKE_NAME));
  }

  /**
   * Test that duplicate IDs throw an exception
   * 
   * @throws IllegalArgumentException always
   */
  @Test(expected = IllegalArgumentException.class)
  public void testDuplicateId() throws IllegalArgumentException {
    Organization o = new Organization();
    Researcher r = new Researcher();
    o.setName(FAKE_NAME);
    r.setName(FAKE_NAME);
    this.model.storeOrganization(o);
    this.model.storeResearcher(r);
    fail(EXPECTED_EXCEPTION);
  }

  /**
   * Test saving data to files and reloading.
   * 
   * @throws IOException on save error
   * @throws JAXBException if a parser cannot be created
   */
  @Test
  public void testSave() throws IOException, JAXBException {
    this.model.setResearchersFile(this.xmlDir + "researchers.save.xml");
    this.model.setOrganizationsFile(this.xmlDir
        + "organizations.save.xml");
    this.model.setCollaborationsFile(this.xmlDir
        + "collaborations.save.xml");
    this.model.save();

    Set<String> ids = new HashSet<String>(this.model.getIds());

    assertEquals("Same contents on reload", ids, this.model.getIds());

    Researcher r = new Researcher();
    Organization o = new Organization();
    Collaboration c = new Collaboration();
    this.model.fillResearcher(r, RESEARCHER_ID);
    this.model.fillOrganization(o, ORG_ID);
    this.model.fillCollaboration(c, COLLAB_ID);
    r.setBioStatement(FAKE_NAME);
    o.setResearchDescription(FAKE_NAME);
    c.setDescription(FAKE_NAME);
    this.model.storeResearcher(r);
    this.model.storeOrganization(o);
    this.model.storeCollaboration(c);

    this.model.save();
    this.model.load();

    r = new Researcher();
    o = new Organization();
    c = new Collaboration();
    this.model.fillResearcher(r, RESEARCHER_ID);
    this.model.fillOrganization(o, ORG_ID);
    this.model.fillCollaboration(c, COLLAB_ID);
    assertEquals("Edited researcher", r.getBioStatement(), FAKE_NAME);
    assertEquals("Editted org", o.getResearchDescription(), FAKE_NAME);
    assertEquals("Edited collab", c.getDescription(), FAKE_NAME);
  }

  /**
   * Test the reference completions.
   * 
   * @throws IOException on bad data
   * @throws JAXBException on parser error
   */
  @Test
  public void testCompletion() throws IOException, JAXBException {
    this.model.setCollaborationsFile(xmlDir
        + "collaborations.completion.xml");
    this.model.setOrganizationsFile(xmlDir
        + "organizations.completion.xml");
    this.model
        .setResearchersFile(xmlDir + "researchers.completion.xml");

    Researcher r = new Researcher();
    Organization o = new Organization();
    Collaboration c = new Collaboration();

    this.model.fillCollaboration(c, COLLAB_ID);
    c.getCollaboratingOrganizations().add(FAKE_NAME + "org1");
    this.model.storeCollaboration(c);

    this.model.fillOrganization(o, ORG_ID);
    o.getAffiliatedResearchers().add(FAKE_NAME + "researcher");
    this.model.storeOrganization(o);

    this.model.fillResearcher(r, RESEARCHER_ID);
    r.setOrganization(FAKE_NAME + "org2");
    this.model.storeResearcher(r);

    this.model.save();
    this.model.load();

    assertTrue("Completed org", this.model.getOrganizations()
        .containsKey(makeId(FAKE_NAME + "org1")));
    assertFalse("Org setting overrides researcher", this.model
        .getOrganizations().containsKey(makeId(FAKE_NAME + "org2")));
    this.model.fillResearcher(r, makeId(FAKE_NAME + "researcher"));
    this.model.fillOrganization(o, ORG_ID);
    assertTrue("Completed researcher", r.getOrganization().equals(
        o.getName()));
  }

  /**
   * Test collaboration filter enums.
   */
  @Test
  public void testCollaborationFilterEnums() {
    assertEquals("Check enum", MyIsernModel.CollabFilter.YEAR,
        MyIsernModel.CollabFilter.valueOf("YEAR"));
    assertEquals("Check enum", MyIsernModel.CollabLevelCompare.EQUAL,
        MyIsernModel.CollabLevelCompare.valueOf("EQUAL"));
  }

  /**
   * Check that copies of data from the model have separate lists.
   */
  @Test
  public void testCopySeparation() {
    Collaboration c1 = new Collaboration();
    Collaboration c2 = new Collaboration();

    this.model.fillCollaboration(c1, COLLAB_ID);
    this.model.fillCollaboration(c2, COLLAB_ID);

    assertNotSame("Separate objects", c1.getYears(), c2.getYears());
    assertEquals("Same size", c1.getYears(), c2.getYears());
    assertEquals("Correct size", 2, c2.getYears().size());
  }

  /**
   * Test that the data integrity report is correct.
   * 
   * @throws IOException on reload error
   * @throws JAXBException on reload error
   */
  @Test
  public void testDataIntegrityReport() throws JAXBException,
      IOException {
    assertEquals("Complete data in example", 0, this.model
        .getDataIntegrityReport().size());

    // researcher
    Researcher r = new Researcher();
    r.setName(FAKE_NAME + "res");
    r.setOrganization(FAKE_NAME + "org");
    this.model.storeResearcher(r);
    assertEquals(
        "Researcher organization, email, picture-link, and bio-statement",
        4, this.model.getDataIntegrityReport().size());

    this.model.load();

    // org
    Organization o = new Organization();
    o.setName(FAKE_NAME + "org");
    this.model.storeOrganization(o);
    assertEquals(
        "Organization type, contact, country, research-keywords, " +
        "research-description, affiliated-researchers home page",
        7, this.model.getDataIntegrityReport().size());

    this.model.load();

    // collab
    Collaboration c = new Collaboration();
    c.setName(FAKE_NAME);
    c.getYears().add(BigInteger.valueOf(2007));
    this.model.storeCollaboration(c);
    assertEquals(
        "Collaboration collaborating-organizations, collaboration-types, " + 
        "outcome-types, description",
        4, this.model.getDataIntegrityReport().size());
  }
}
